//
//  TTDatabase.m
//  Tint
//
//  Created by Kevin Wu on 2/28/12.
//  Copyright (c) 2012 Telligenty. All rights reserved.
//

#import "TTDatabase.h"

@interface TTDatabase (Private)
- (BOOL)bindStatement:(sqlite3_stmt *)statement toParameters:(NSArray *)parameters;
- (void)bindObject:(id)object toColumn:(int)index inStatement:(sqlite3_stmt *)statement;
@end

@implementation TTDatabase (Private)

- (BOOL)bindStatement:(sqlite3_stmt *)statement toParameters:(NSArray *)parameters {
	int queryCount = sqlite3_bind_parameter_count(statement);
	
    int index = 0;
	for(id object in parameters) {
		index++;
		[self bindObject:object toColumn:index inStatement:statement];
	}
	
	return index == queryCount;
}

- (void)bindObject:(id)object toColumn:(int)index inStatement:(sqlite3_stmt *)statement {
	if ((!object) || ((NSNull *)object == [NSNull null])) {
		sqlite3_bind_null(statement, index);
	} else if ([object isKindOfClass:[NSData class]]) {
		sqlite3_bind_blob(statement, index, [object bytes], [object length], SQLITE_STATIC);
	} else if ([object isKindOfClass:[NSDate class]]) {
		sqlite3_bind_double(statement, index, [object timeIntervalSince1970]);
	} else if ([object isKindOfClass:[NSNumber class]]) {
		if (strcmp([object objCType], @encode(BOOL)) == 0) {
			sqlite3_bind_int(statement, index, ([object boolValue] ? 1 : 0));
		} else if (strcmp([object objCType], @encode(int)) == 0) {
			sqlite3_bind_int64(statement, index, [object longValue]);
		} else if (strcmp([object objCType], @encode(long long)) == 0) {
			sqlite3_bind_int64(statement, index, [object longLongValue]);
		} else if (strcmp([object objCType], @encode(float)) == 0) {
			sqlite3_bind_double(statement, index, [object floatValue]);
		} else if (strcmp([object objCType], @encode(double)) == 0) {
			sqlite3_bind_double(statement, index, [object doubleValue]);
		} else {
			sqlite3_bind_text(statement, index, [[object description] UTF8String], -1, SQLITE_STATIC);
		}
	} else {
		sqlite3_bind_text(statement, index, [[object description] UTF8String], -1, SQLITE_STATIC);
	}
}

@end



@implementation TTDatabase


#pragma mark - Accessors

@synthesize handle = _handle;
@synthesize databasePath = _databasePath;



#pragma mark - Memory

- (id)init {
    self = [super init];
    if (self) {
        _handle = NULL;
        _databasePath = nil;
        
        _opened = NO;
        
        _executeLock = [[NSLock alloc] init];
    }
    return self;
}

- (void)dealloc {
    [self close];
    
    _handle = NULL;
    TTRelease(_databasePath);
    
    _opened = NO;
    
    TTRelease(_executeLock);
    [super dealloc];
}



#pragma mark - Singleton

+ (TTDatabase *)sharedObject {
    static TTDatabase *sharedDatabase = nil;
    if (sharedDatabase == nil) {
        sharedDatabase = [[self alloc] init];
    }
    return sharedDatabase;
}

#pragma mark - Sqlite connection

- (BOOL)open {
	if(_opened) return YES;
	
	int error = sqlite3_open([_databasePath fileSystemRepresentation], &_handle);
	if(error != SQLITE_OK)
        return NO;
	
	_opened = YES;
	return YES;
}

- (void)close {
	if(!_handle) return;
	sqlite3_close(_handle);
	_opened = NO;
}

- (BOOL)goodConnection {
    if (!_handle) return NO;
    
    return ([self executeQueryString:@"SELECT * FROM sqlite_master WHERE type='table';"] != nil);
}



#pragma mark - Execute update

- (BOOL)executeUpdateString:(NSString *)sql {
    return [self executeUpdate:sql, nil];
}

- (BOOL)executeUpdate:(NSString *)sql, ... {
    
    NSMutableArray *valistArray = nil;
    
    if (TTIsStringWithText(sql)) {
        valistArray = [NSMutableArray array];
        va_list arguments;
        va_start(arguments, sql);
        id object = nil;
        while ((object = va_arg(arguments, id)))
            [valistArray addObject:object];
        va_end(arguments);
    }
    
	return [self executeUpdate:sql parameters:valistArray];
}

- (BOOL)executeUpdate:(NSString *)sql parameters:(NSArray *)parameters {
	[_executeLock lock];
    
	if(![self open]) {
		[_executeLock unlock];
		return NO;
	}
    
	int returnCode = 0;
    
	sqlite3_stmt *statement = NULL;
	returnCode = sqlite3_prepare(_handle, [sql UTF8String], -1, &statement, 0);
	if (returnCode == SQLITE_BUSY) {
		[_executeLock unlock];
        TTDPRINT(@"[Update] Database busy while preparing: %@", sql);
		return NO;
	} else if (returnCode != SQLITE_OK) {
		sqlite3_finalize(statement);
		[_executeLock unlock];
        TTDPRINT(@"[Update] Query failed while preparing: %@", sql);
		return NO;
	}
	
	
	if (![self bindStatement:statement toParameters:parameters]) {
		sqlite3_finalize(statement);
		[_executeLock unlock];
        TTDPRINT(@"[Update] Bind failed: %@", sql);
		return NO;
	}
    
    returnCode = sqlite3_step(statement);
    
    if (SQLITE_BUSY == returnCode) {
		TTDPRINT(@"[Update] Database Busy while stepping: %@", sql);
	} else if (SQLITE_DONE == returnCode || SQLITE_ROW == returnCode) {
		
	} else if (SQLITE_ERROR == returnCode) {
        TTDPRINT(@"[Update] SQLITE_ERROR  while stepping: %@", sql);
	} else if (SQLITE_MISUSE == returnCode) {
        TTDPRINT(@"[Update] SQLITE_MISUSE  while stepping: %@", sql);
	} else {
        TTDPRINT(@"[Update] UNKNOWN_ERROR  while stepping: %@", sql);
	}
    
	returnCode = sqlite3_finalize(statement);
    
	[_executeLock unlock];
    
	return (returnCode == SQLITE_OK);
}



#pragma mark - Execute insert

- (sqlite3_int64)executeInsertString:(NSString *)sql {
    return [self executeInsert:sql, nil];
}

- (sqlite3_int64)executeInsert:(NSString *)sql, ... {
    
    NSMutableArray *valistArray = nil;
    
    if (TTIsStringWithText(sql)) {
        valistArray = [NSMutableArray array];
        va_list arguments;
        va_start(arguments, sql);
        id object = nil;
        while ((object = va_arg(arguments, id)))
            [valistArray addObject:object];
        va_end(arguments);
    }
    
    return [self executeInsert:sql parameters:valistArray];
}

- (sqlite3_int64)executeInsert:(NSString *)sql parameters:(NSArray *)parameters {
    
    if (![[sql uppercaseString] hasPrefix:@"INSERT INTO"]) return 0;
    
    [_executeLock lock];
    
	if(![self open]) {
		[_executeLock unlock];
		return 0;
	}
    
	int returnCode = 0;
    
	sqlite3_stmt *statement = NULL;
	returnCode = sqlite3_prepare(_handle, [sql UTF8String], -1, &statement, 0);
	if (returnCode == SQLITE_BUSY) {
		[_executeLock unlock];
        TTDPRINT(@"[Insert] Database busy while preparing: %@", sql);
		return 0;
	} else if (returnCode != SQLITE_OK) {
		sqlite3_finalize(statement);
		[_executeLock unlock];
        TTDPRINT(@"[Insert] Query failed while preparing: %@", sql);
		return 0;
	}
	
	
	if (![self bindStatement:statement toParameters:parameters]) {
		sqlite3_finalize(statement);
		[_executeLock unlock];
        TTDPRINT(@"[Insert] Bind failed: %@", sql);
		return 0;
	}
    
    returnCode = sqlite3_step(statement);
    
    if (SQLITE_BUSY == returnCode) {
		TTDPRINT(@"[Insert] Database Busy while stepping: %@", sql);
	} else if (SQLITE_DONE == returnCode || SQLITE_ROW == returnCode) {
		
	} else if (SQLITE_ERROR == returnCode) {
        TTDPRINT(@"[Insert] SQLITE_ERROR while stepping: %@", sql);
	} else if (SQLITE_MISUSE == returnCode) {
        TTDPRINT(@"[Insert] SQLITE_MISUSE while stepping: %@", sql);
	} else {
        TTDPRINT(@"[Insert] UNKNOWN_ERROR while stepping: %@", sql);
	}
    
	returnCode = sqlite3_finalize(statement);
    
    sqlite3_int64 rowid = sqlite3_last_insert_rowid(_handle);
    
	[_executeLock unlock];
    
    if (returnCode == SQLITE_OK) {
        return rowid;
    }
    
    return 0;
}



#pragma mark - Execute query

- (TTDatabaseResult *)executeQueryString:(NSString *)sql {
    return [self executeQuery:sql, nil];
}

- (TTDatabaseResult *)executeQuery:(NSString *)sql, ... {
    
    NSMutableArray *valistArray = nil;
    
    if (TTIsStringWithText(sql)) {
        valistArray = [NSMutableArray array];
        va_list arguments;
        va_start(arguments, sql);
        id object = nil;
        while ((object = va_arg(arguments, id)))
            [valistArray addObject:object];
        va_end(arguments);
    }
    
	return [self executeQuery:sql parameters:valistArray];
}

- (TTDatabaseResult *)executeQuery:(NSString *)sql parameters:(NSArray *)parameters {
	[_executeLock lock];
    
    if(![self open]) {
		[_executeLock unlock];
		return nil;
	}
    
	
    TTDatabaseResult *result = [[TTDatabaseResult alloc] init];
    
	int returnCode = 0;
    
	sqlite3_stmt *statement = NULL;
	returnCode = sqlite3_prepare(_handle, [sql UTF8String], -1, &statement, 0);
	result.errorCode = [self lastErrorCode];
	result.errorMessage = [self lastErrorMessage];
	
	if (returnCode == SQLITE_BUSY) {
		[_executeLock unlock];
        TTDPRINT(@"[Query] Database busy while preparing: %@", sql);
        TTRelease(result);
		return nil;
	} else if (returnCode != SQLITE_OK) {
		sqlite3_finalize(statement);
		[_executeLock unlock];
        TTDPRINT(@"[Query] Query failed while preparing: %@", sql);
        TTRelease(result);
		return nil;
	}
	
	if (![self bindStatement:statement toParameters:parameters]) {
		sqlite3_finalize(statement);
		[_executeLock unlock];
        TTDPRINT(@"[Query] Bind failed: %@", sql);
        TTRelease(result);
		return nil;
	}
	
	int columnCount = sqlite3_column_count(statement);
	for(int x=0; x<columnCount; x++) {
		if(sqlite3_column_name(statement, x) != NULL) {
			[result.columnNames addObject:[NSString stringWithUTF8String:sqlite3_column_name(statement, x)]];
		} else {
			[result.columnNames addObject:[NSString stringWithFormat:@"%d", x]];
		}
        
		if(sqlite3_column_decltype(statement, x) != NULL) {
			[result.columnTypes addObject:[NSString stringWithUTF8String:sqlite3_column_decltype(statement, x)]];
		} else {
			[result.columnTypes addObject:@""];
		}
	}
	
	while(sqlite3_step(statement) == SQLITE_ROW) {
		TTDatabaseRow *row = [[TTDatabaseRow alloc] initWithDatabaseResult:result];
		for(int x=0; x<columnCount; x++) {
			if(sqlite3_column_text(statement, x) != NULL) {
				[row.columnData addObject:[NSString stringWithUTF8String:(const char *)sqlite3_column_text(statement, x)]];
			} else {
				[row.columnData addObject:@""];
			}
		}
		
		[result addRow:row];
        TTRelease(row);
	}
	
	sqlite3_finalize(statement);
    
	[_executeLock unlock];
    
	return [result autorelease];
}


#pragma mark - Error methods

- (NSString *)lastErrorMessage {
    return [NSString stringWithUTF8String:sqlite3_errmsg(_handle)];
}

- (int)lastErrorCode {
    return sqlite3_errcode(_handle);
}

- (BOOL)hadError {
    int lastErrCode = [self lastErrorCode];
    return (lastErrCode > SQLITE_OK && lastErrCode < SQLITE_ROW);
}



#pragma mark - Sqlite ability

+ (BOOL)isSQLiteThreadSafe {
    // Make sure to read the sqlite headers on this guy!
    return sqlite3_threadsafe();
}

+ (NSString *)sqliteLibVersion {
    return [NSString stringWithFormat:@"%s", sqlite3_libversion()];
}

- (BOOL)hasTable:(NSString *)tableName {
    if (!_handle) return NO;
    if (!tableName) return NO;
    
    NSString *sql = @"SELECT * FROM sqlite_master WHERE type='table' AND name=?;";
    TTDatabaseResult *result = [self executeQuery:sql, tableName, nil];
    return ([result count]>0);
}

@end

